package biback

import java.text.SimpleDateFormat
import java.util.{Calendar, GregorianCalendar}

import biback.domain.account.BalanceType
import biback.domain.common.TrnContext
import biback.domain.refined._
import biback.utils._
import cats.{Monad, Order}
import eu.timepit.refined._
import eu.timepit.refined.api._
import eu.timepit.refined.auto._
import eu.timepit.refined.boolean.And
import eu.timepit.refined.numeric._
import eu.timepit.refined.string._
import eu.timepit.refined.types.numeric._

package object domain {

  type BiDec = BigDecimal

  type NnRule = NonNegative

  type NnDec = NonNegBigDecimal
  type NnInt = NonNegInt
  type NnNum = NonNegLong

  type PoRule = Positive

  type PoDec = PosBigDecimal
  type PoInt = PosInt
  type PoNum = PosLong

  val NnInt0: NnInt = 0
  val NnInt1: NnInt = 1
  val NnNum0: NnNum = 0L
  val NnNum1: NnNum = 1L
  val NnDec0: NnDec = BigDecimal(0.00)
  val NnDec1: NnDec = BigDecimal(1.00)
  val BiDec0: BiDec = BigDecimal(0.00)
  val BiDec1: BiDec = BigDecimal(1.00)

  type DateRule = ValidDate[W.`"""yyyyMMdd"""`.T]
  type TrnDate = String Refined DateRule

  type DateTimeRule = ValidDate[W.`"""yyyyMMddHHmmss"""`.T]
  type DateTime = String Refined DateTimeRule

  type TermPayDayRule = GreaterEqual[W.`1`.T] And LessEqual[W.`28`.T]
  type TermPayDay = Int Refined TermPayDayRule

  type SMSVCodeRule = MatchesRegex[W.`"""^(\\d{6})$"""`.T]
  type SMSVCode = String Refined SMSVCodeRule

  type ChinaMobileRule = MatchesRegex[W.`"""^1(\\d{10})$"""`.T]
  type Mobile = String Refined ChinaMobileRule

  type TrnCodeRule = MatchesRegex[W.`"""^[FMQ](\\d{6})$"""`.T]
  type TrnCode = String Refined TrnCodeRule

  type PhoneRule = Trimmed
  type Phone = String Refined PhoneRule

  type SubAccountNoRule = MatchesRegex[W.`"""^(\\d{5})$"""`.T]
  type SubAccountNo = String Refined SubAccountNoRule

  type CustomerNoRule = Trimmed
  type CustomerNo = String Refined CustomerNoRule

  type PersonalCustomerNoRule = Trimmed
  type PersonalCustomerNo = String Refined PersonalCustomerNoRule

  type IdNoRule = Trimmed
  type IdNo = String Refined IdNoRule

  type BankNoRule = Trimmed
  type BankNo = String Refined BankNoRule

  type AccountNoRule = Trimmed
  type AccountNo = String Refined AccountNoRule // 账号或卡号

  type AccountIdRule = Trimmed
  type AccountId = String Refined AccountIdRule

  type CardNoRule = Trimmed
  type CardNo = String Refined CardNoRule

  type CertificateNoRule = Trimmed
  type CertificateNo = String Refined CertificateNoRule

  type TicketNoRule = Trimmed
  type TicketNo = String Refined TicketNoRule

  type BranchNoRule = Trimmed
  type BranchNo = String Refined BranchNoRule

  type DeptNoRule = Trimmed
  type DeptNo = String Refined DeptNoRule

  type ProductCodeRule = MatchesRegex[W.`"""^(\\d{3})$"""`.T]
  type ProductCode = String Refined ProductCodeRule

  type FeatureCdRule = Trimmed
  type FeatureCd = String Refined FeatureCdRule

  type TellerNoRule = Trimmed
  type TellerNo = String Refined TellerNoRule

  type MerchantNoRule = Trimmed
  type MerchantNo = String Refined MerchantNoRule

  type SubjectNoRule = MatchesRegex[W.`"""^(\\d{4,8})$"""`.T]
  type SubjectNo = String Refined SubjectNoRule

  type NameRule = Trimmed
  type Name = String Refined NameRule

  type NationCodeRule = Trimmed
  type NationCode = String Refined NationCodeRule

  type ProvinceCodeRule = Trimmed
  type ProvinceCode = String Refined ProvinceCodeRule

  type DistrictCodeRule = Trimmed
  type DistrictCode = String Refined DistrictCodeRule

  type CityCodeRule = Trimmed
  type CityCode = String Refined CityCodeRule

  type AreaCodeRule = Trimmed
  type AreaCode = String Refined AreaCodeRule

  type CcyCdRule = ValidCcy
  type CcyCd = String Refined CcyCdRule

  type TrnIdRule = Uuid
  type TrnId = String Refined TrnIdRule

  type VoucherTypeRule = Trimmed
  type VoucherType = String Refined VoucherTypeRule

  type Cipher16Rule = MatchesRegex[W.`"""^([A-Fa-f0-9]{16})$"""`.T]
  type Cipher16 = String Refined Cipher16Rule

  type IntPriceNoRule = MatchesRegex[W.`"""^(\\d{6})$"""`.T]
  type IntPriceNo = String Refined IntPriceNoRule

  type FeeNoRule = MatchesRegex[W.`"""^(\\d{4})$"""`.T]
  type FeeNo = String Refined FeeNoRule

  abstract class DomainService[F[_] : Monad, RQ, RS] extends GeneralAlgebra[F] {
    def run(ctx: TrnContext, rq: RQ): ErrOrF[RS]
  }

  trait DomainCommand {
    val ctx: TrnContext
  }

  trait DomainEvent

  final case class InvalidEvent(e: DomainEvent) extends Exception with Product with Serializable

  implicit val o1: Order[BalanceType] = (x: BalanceType, y: BalanceType) => x.entryName.compareTo(y.entryName)

  implicit val o2: Order[SubAccountNo] = (x: SubAccountNo, y: SubAccountNo) => x.value.compareTo(y.value)

  implicit val o3: Order[TrnDate] = (x: TrnDate, y: TrnDate) => x.value.compareTo(y.value)

  implicit val o4: Ordering[TrnDate] = (x: TrnDate, y: TrnDate) => x.value.compareTo(y.value)

  implicit class TrnDateOps(a: TrnDate) {
    val sdf = new SimpleDateFormat("yyyyMMdd")
    def toDate(dt: TrnDate): GregorianCalendar = {
      val dtX = new GregorianCalendar()
      dtX.setTime(sdf.parse(dt.value))
      dtX
    }
    val ga = toDate(a)
    def nextDay(n: Int): Either[DomainError, TrnDate] = {
      ga.add(Calendar.DATE, n)
      sdf.format(ga.getTime).check[DateRule]
    }
    def isLeap(i: Int): Boolean = i % 4 == 0 && i % 100 != 0 || i % 400 == 0
    def daysOfYear(i: Int): Int = if (isLeap(i)) 366 else 365
    def -(b: TrnDate): Int = {
      val gb = toDate(b)
      val dayA = ga.get(Calendar.DAY_OF_YEAR)
      val dayB = gb.get(Calendar.DAY_OF_YEAR)
      val yearA = ga.get(Calendar.YEAR)
      val yearB = gb.get(Calendar.YEAR)
      if (yearA > yearB) (yearB until yearA).map(daysOfYear).sum + (dayA - dayB)
      else if (yearA < yearB) (yearA until yearB).map(daysOfYear).sum + (dayB - dayA)
      else dayA - dayB
    }
    def <(b: TrnDate): Boolean = o4.compare(a, b) < 0
    def <=(b: TrnDate): Boolean = o4.compare(a, b) <= 0
    def nearestDay(n: Int): Either[DomainError, TrnDate] = {
      for {
        _ <- n.check[TermPayDayRule]
        yearA = ga.get(Calendar.YEAR)
        monthA = ga.get(Calendar.MONTH)
        dt = if (ga.get(Calendar.DAY_OF_MONTH) < n)
          new GregorianCalendar(yearA, monthA, n)
        else if (monthA == 11)
          new GregorianCalendar(yearA + 1, 0, n)
        else
          new GregorianCalendar(yearA, monthA + 1, n)
        x <- sdf.format(dt.getTime).check[DateRule]
      } yield x
    }
    def nextTerm(n: Int = 1): Either[DomainError, TrnDate] = {
      val monthA = ga.get(Calendar.MONTH) + n
      val dayA = ga.get(Calendar.DAY_OF_MONTH)
      val yearA = ga.get(Calendar.YEAR) + monthA / 12
      sdf.format(new GregorianCalendar(yearA, monthA % 12, dayA).getTime).check[DateRule]
    }
  }

  implicit class BigDecimalOps(d: BiDec) {
    def floor(unit: BiDec): BiDec = (d /% unit)._1 * unit
    def floorX(scale: NnInt): BiDec = {
      val unit = BigDecimal(10.00).pow(- scale.value)
      ((d /% unit)._1 * unit).setScale(scale)
    }
    def roundX(scale: NnInt): BiDec = {
      val unit = BigDecimal(10.00).pow(- scale.value)
      val (a, b) = d /% unit
      val r = if (b > unit / 2) unit else BigDecimal(0.00)
      (a * unit + r).setScale(scale.value)
    }
    def format(implicit width: Int): String =
      "".padTo(width - d.toString.length, ' ') + d.toString
  }

  implicit class NumberOps(d: Int) {
    def format(width: Int = 2, ch: Char = '0'): String =
      "".padTo(width - d.toString.length, ch) + d.toString
  }

  implicit class NonNegBigDecimalOps(d: NnDec) {
    def floor(unit: BiDec): BiDec = (d.value /% unit)._1 * unit
    def floorX(unit: NnDec): BiDec = (d.value /% unit.value)._1 * unit.value
  }
}
